KM.Renderer = function( gl )
{
    // WebGL Context
    this._gl = gl;
    
    // Vertices buffers
    this._geometryVerticesBuffersMap = {};
    
    // Indices buffers
    this._trianglesIndicesBuffersMap = {};
    
    // Normals buffer
    this._geometryNormalsBuffersMap = {};
    
    // To push / pop modelView matrix & co
    this._transformsManager = KM.TransformsManager.GetInstance();
};

KM.Renderer.CLEAR_COLOR_RGB = vec3.create( [ 0.0, 0.0, 0.0 ] );
KM.Renderer.CLEAR_ALPHA = 1.0;

KM.Renderer.prototype = {
    constructor: KM.Renderer,
    
    init: function( iCallback )
    {
        this.setClearColor( KM.Renderer.CLEAR_COLOR_RGB[ 0 ], KM.Renderer.CLEAR_COLOR_RGB[ 1 ], KM.Renderer.CLEAR_COLOR_RGB[ 2 ], KM.Renderer.CLEAR_ALPHA );
        this.enableDepthTest();
        
        KM.ShadersManager.loadShaders( this._gl, shadersDescriptions, iCallback );
    },
    
    setClearColor: function( iRed, iGreen, iBlue, iAlpha )
    {
        this._gl.clearColor( iRed, iGreen, iBlue, iAlpha );
    },
    
    enableDepthTest: function()
    {
        this._gl.enable( this._gl.DEPTH_TEST );    
    },
    
    setViewport: function( iX, iY, iWidth, iHeight )
    {
        this._gl.viewport( iX, iY, iWidth, iHeight );
    },
    
    clearBuffers: function( iMask )
    {
        this._gl.clear( iMask );
    },
    
    render: function( iScene, iCamera )
    {
        // Setting viewport
        var iCameraViewport = iCamera._viewport;
        this.setViewport( 0, 0, iCameraViewport._width, iCameraViewport._height );
        
        // Clear
        this.clearBuffers( this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT );
        
        // Scene lights
        var iLights = iScene._lights;
        
        // Scene objects
        var iObjects = iScene._objects;
        var iObjectsNb = iScene._objectsNb;
        for( var i = 0; i < iObjectsNb; i++ )
        {
            var iObject = iObjects[ i ];
            
            // Saving modelView transform
            this._transformsManager.pushMV();
            
            this.renderObject( iCamera, iLights, iObject );
            
            // Restoring modelView transform
            this._transformsManager.popMV();
        }
    },
    
    renderObject: function( iCamera, iLights, iObject )
    {
//        console.log("Renderer.js, renderObject() : rendering " + iObject + " ...");
        
        // Camera projection
        var iCameraProjection = iCamera._projection;
        
        // Camera transform
        var iCameraTransform = iCamera._transform;
        
        // Object transform
        var iObjectTransform = iObject._transform;
        
        // View matrix 
        this._transformsManager._viewMatrix = mat4.create( iCameraTransform._matrix );
        
        // Model coordinates -> World coordinates -> Camera coordinates 
        // ModelView matrix = Model (Object) matrix * View (Camera) matrix => Eye Space
        this._transformsManager._modelViewTransform = iCameraTransform.multiply( iObjectTransform );
        
        // Getting the corresponding shader
        var iObjectShaderDescriptionName = iObject._shaderDescriptionName;
        var shaderProgram = KM.ShadersManager.getShaderProgram( iObjectShaderDescriptionName );
        if( !shaderProgram )
        {
            console.error("Renderer.js, renderObject() : Can't get shaderProgram for " + iObjectShaderDescriptionName );
            return;
        }
        
        // Binding shader
        KM.ShadersManager.bindShader( this._gl, shaderProgram );
        
        // Sending datas to GPU
        this.setShaderProgramUniforms( shaderProgram, iCameraProjection, this._transformsManager._modelViewTransform, this._transformsManager._viewMatrix, iLights, iObject._material );
        
        // Rendering geometry
        this.renderGeometry( shaderProgram, iObject._geometry ) ;
        
        // Unbinding shader
        KM.ShadersManager.unbindShader( this._gl );
    },
    
    setShaderProgramUniforms: function( iShaderProgram, iCameraProjection, iModelViewTransform, iViewMatrix, iLights, iMaterial )
    {
//        console.log("Renderer.js, setShaderProgramUniforms() : binding shaderProgram " + shaderProgram._name
//                + ", \n\t CameraProjection: " + cameraProjection
//                + ", \n\t ModelView: " + iModelViewTransform 
//                + ", \n\t lightsNb => " + lights.length
//                + ", \n\t" + material);
        
        // Projection matrix
        this._gl.uniformMatrix4fv( iShaderProgram._pMatrixUniform, false, iCameraProjection._matrix );
        
        // ModelView matrix
        this._gl.uniformMatrix4fv( iShaderProgram._mvMatrixUniform, false, iModelViewTransform._matrix );
        
        // View matrix
        this._gl.uniformMatrix4fv( iShaderProgram._viewMatrixUniform, false, iViewMatrix );
        
        // Are lightings ?
        if( !iShaderProgram._isLightings ) return;
        
        // Normal matrix
        iModelViewTransform.getNormalMatrix( this._transformsManager._normalTransform._matrix );
        this._gl.uniformMatrix4fv( iShaderProgram._nMatrixUniform, false, this._transformsManager._normalTransform._matrix );
        
        // Scene lightings
        var lightsNb = iLights.length;
        for( var i = 0; i < lightsNb; i++ )
        {
            var light = iLights[ i ];
            var lightColor = light._color;;
            
            if( light instanceof KM.AmbientLight )
            {
                // Color
                this._gl.uniform3f( iShaderProgram._ambientColorUniform, lightColor._r, lightColor._g, lightColor._b );
            }
            else if( light instanceof KM.DirectionalLight )
            {
                // Color
                this._gl.uniform3f( iShaderProgram._directionalLightColorUniform, lightColor._r, lightColor._g, lightColor._b );
                
                // Direction TODO : Multiply by View Matrix <=> Camera matrix
//                var dirLight = vec3.create( [ light._direction[ 0 ], light._direction[ 1 ], light._direction[ 2 ] ] );
//                mat4.multiplyVec3( mat4.inverse( camera._transform._matrix, mat4.create() ), dirLight );
//                vec3.normalize( dirLight );
                this._gl.uniform3fv( iShaderProgram._directionalLightDirectionUniform, light._direction );
            }
        }
        
        // Emissive 
        var emissive = iMaterial._emissive;
        this._gl.uniform3f( iShaderProgram._KeUniform, emissive._r, emissive._g, emissive._b );
        
        // Ambient 
        var ambient = iMaterial._ambient;
        this._gl.uniform3f( iShaderProgram._KaUniform, ambient._r, ambient._g, ambient._b );
        
        // Diffuse
        var diffuse = iMaterial._diffuse;
        this._gl.uniform3f( iShaderProgram._KdUniform, diffuse._r, diffuse._g, diffuse._b );
        
        // Specular
        var specular = iMaterial._specular;
        this._gl.uniform3f( iShaderProgram._KsUniform, specular._r, specular._g, specular._b );
        
        // Alpha
        this._gl.uniform1f( iShaderProgram._AlphaUniform, iMaterial._alpha );
    },
    
    renderGeometry: function( shaderProgram, geometry )
    {
//        console.log("Renderer.js, renderGeometry() : rendering " + geometry + " ...");
        
        // Vertices
        if( !this._geometryVerticesBuffersMap[ geometry ] )
        {
            this._geometryVerticesBuffersMap[ geometry ] = this.createVerticesBufferForGeometry( geometry );
        }
        
        // Binding vertices buffer
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._geometryVerticesBuffersMap[ geometry ] );
        this._gl.vertexAttribPointer( shaderProgram._vertexPositionAttribute, 3, this._gl.FLOAT, false, 0, 0 );
        
        // Are lightings ?
        if( shaderProgram._isLightings )
        {
            // Normals vertices buffer
            if( !this._geometryNormalsBuffersMap[ geometry ] )
            {
                this._geometryNormalsBuffersMap[ geometry ] = this.createNormalsBufferForGeometry( geometry );
            }

            // Binding normals buffer
            this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._geometryNormalsBuffersMap[ geometry ] );
            this._gl.vertexAttribPointer( shaderProgram._vertexNormalAttribute, 3, this._gl.FLOAT, false, 0, 0 );
        }
        
        
        // Triangles
        var triangles = geometry._triangles;
        var trianglesNb = geometry._trianglesNb;
        
        for( var i = 0; i < trianglesNb; i++ )
        {
            var triangle = triangles[ i ];
            this.renderTriangle( triangle );
        }
        
        // Unbinding vertices / normals buffer
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, null );
    },
    
    renderTriangle: function( triangle )
    {
//        console.log("Renderer.js, renderTriangle() : rendering " + triangle + "...");
        
        if( !this._trianglesIndicesBuffersMap[ triangle ] )
        {
            this._trianglesIndicesBuffersMap[ triangle ] = this.createIndicesBufferForTriangle( triangle );
        }
        
        // Binding indices buffer
        this._gl.bindBuffer( this._gl.ELEMENT_ARRAY_BUFFER, this._trianglesIndicesBuffersMap[ triangle ] );
        
        // Drawing
        this._gl.drawElements( this._gl.TRIANGLES, 3, this._gl.UNSIGNED_SHORT, 0 );
        
        // Unbinding indices buffer
        this._gl.bindBuffer( this._gl.ELEMENT_ARRAY_BUFFER, null );
    },
    
    createVerticesBufferForGeometry: function( geometry )
    {
//        console.log("Renderer.js, createVerticesBufferForTriangle() : Creating vertices buffer for " + geometry + " ...");
        
        var buffer = this._gl.createBuffer();
        
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, buffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( geometry._vertices ), this._gl.STATIC_DRAW );     
        
        return buffer;
    },
    
    createNormalsBufferForGeometry: function( geometry )
    {
//        console.log("Renderer.js, createNormalsBufferForGeometry() : Creating normals buffer for " + geometry + " ...");
        
        var buffer = this._gl.createBuffer();
        
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, buffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( geometry._normals ), this._gl.STATIC_DRAW ); 
        
        return buffer;
    },
    
    createIndicesBufferForTriangle: function( triangle )
    {
//        console.log("Renderer.js, createIndicesBufferForTriangle() : Creating indices buffer for " + triangle + " ...");
        
        var buffer = this._gl.createBuffer();
        
        this._gl.bindBuffer( this._gl.ELEMENT_ARRAY_BUFFER, buffer );
        this._gl.bufferData( this._gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( triangle._indices ), this._gl.STATIC_DRAW );  
        
        return buffer;
    },
    
    toString: function()
    {
        return "Renderer";
    }
};
